package instrumenter;


import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;

import javax.swing.JOptionPane;

import org.apache.bcel.Constants;
import org.apache.bcel.Repository;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.generic.*;

import cfg.model.CFGNode;
import br.jabuti.instrumenter.ASMInstrumenter;

/**
 * class to instrument a method execution
 * */
public class MethodExecutionInstrumenter {	
	
	private ASMInstrumenter jabutiInstrumenter; 			// instrumenter
	private Class<?> declaringClass;						// method's declaring class
	private org.apache.bcel.classfile.Method bcelMethod;	// bcel representation of the method
	private MethodGen methodGen;							// method generator
	private ClassGen classGen;								// class generator
	private JavaClass javaClass;							// bcel representation of class
	private ConstantPoolGen constantPoolGen;				//constant pool gen
	
	public MethodExecutionInstrumenter(Method javaMethod) throws ClassNotFoundException{
		declaringClass = javaMethod.getDeclaringClass();
		javaClass = Repository.lookupClass(declaringClass);
		bcelMethod = javaClass.getMethod(javaMethod);
		constantPoolGen = new ConstantPoolGen(bcelMethod.getConstantPool());
		methodGen = new MethodGen(bcelMethod, declaringClass.getCanonicalName(), constantPoolGen);
		classGen = new ClassGen(javaClass);
		jabutiInstrumenter = new ASMInstrumenter(methodGen,classGen, constantPoolGen);
	}
	
	
	public void instrument(CFGNode cfg) throws ClassNotFoundException{
		List<InstructionHandle> instructions = new ArrayList<InstructionHandle>();
		Set<CFGNode> toProcess = allNodes(cfg,new HashSet<CFGNode>());
		for(CFGNode node : toProcess){
			instructions.add(node.getInstructions().get(0));
		}
		this.instrument(instructions);
	}
	
	public static Set<CFGNode> allNodes(CFGNode c, Set<CFGNode> partial){
		if(!partial.contains(c)){
			partial.add(c);
			for(CFGNode atual : c.getChildNodes().keySet()){
				partial = allNodes(atual,partial);
			}
		}
		return partial;
	}
	
	public void instrument(List<InstructionHandle> instructionsToInstrument) throws ClassNotFoundException{
		//add a class containing one static method that will manage to log the executing bytecode number
		Class<SysLogger> loggerC = SysLogger.class;
		Method[] methods = loggerC.getDeclaredMethods();//should be just one
		org.apache.bcel.classfile.Method preLogger= null ,postLogger = null;
		for(Method m : methods){
			if(m.getName().equals("preLogger")){
				preLogger = Repository.lookupClass(loggerC).getMethod(m);
			} else if(m.getName().equals("postLogger")){
				postLogger = Repository.lookupClass(loggerC).getMethod(m);
			}
		}
		
		//logger = bcelMethod;
		for(InstructionHandle instructionToInstrument : instructionsToInstrument){
			InstructionFactory iFactory = new InstructionFactory(classGen,constantPoolGen);
			//add a constant to the pool
			//load one integer containing the bytecode number
			//Instruction iload = new ILOAD(idx);
			//int idx = constantPoolGen.addInteger(instructionToInstrument.getPosition());
			Instruction byteCodeNumber = iFactory.createConstant(instructionToInstrument.getPosition());
			//calls method logger
			//Instruction callLogger = iFactory.createInvoke(logger.getClass().getCanonicalName(),logger.getName(),logger.getReturnType(),logger.getArgumentTypes(),Constants.INVOKESTATIC);
			System.out.println("creating invoke for:" +
					SysLogger.class.getCanonicalName()+"\n"+
					preLogger.getName()+"\n"+
					preLogger.getReturnType()+"\n"+
					preLogger.getArgumentTypes());
			Instruction callPreLogger = iFactory.createInvoke(SysLogger.class.getCanonicalName(),preLogger.getName(),preLogger.getReturnType(),preLogger.getArgumentTypes(),Constants.INVOKESTATIC);
			Instruction callPostLogger = iFactory.createInvoke(SysLogger.class.getCanonicalName(),postLogger.getName(),postLogger.getReturnType(),postLogger.getArgumentTypes(),Constants.INVOKESTATIC);
			System.out.println("creating invoke for:" +
					SysLogger.class.getCanonicalName()+"\n"+
					preLogger.getName()+"\n"+
					preLogger.getReturnType()+"\n"+
					preLogger.getArgumentTypes());
			jabutiInstrumenter.addBefore(
					jabutiInstrumenter.addBefore(
								instructionToInstrument, 
								callPreLogger),
					byteCodeNumber);
			
			jabutiInstrumenter.addBefore(
					jabutiInstrumenter.insertAfter(
								instructionToInstrument, 
								callPostLogger),
					byteCodeNumber);
			
		}
	}
	
	
	public MethodGen getMethodGen(){
		return this.methodGen;
	}
	
	
	
	public void replace(){
		org.apache.bcel.classfile.Method[] methods = this.javaClass.getMethods();
		int i=0;
		for(i=0;i<methods.length;++i){
			if(methods[i]==bcelMethod){
				methods[i]=methodGen.getMethod();
				break;
			}
		}
		classGen.setMethods(methods);
		classGen.setConstantPool(constantPoolGen);
		//methodGen.stripAttributes(true);
		//methodGen.setMaxStack();
		//methodGen.setMaxLocals();
		
	}

	public ClassGen getClassGen() {
		return this.classGen;
	}
	
	public void dump(String fileName) throws IOException{
		dump(new File(fileName));
	}
	
	/**
	 * Dumps this method to a new .class file
	 * @param baseDir the file to dump to
	 * @throws IOException
	 * 
	 * */
	public void dump(File baseDir) throws IOException{
		this.replace();
		JavaClass c1 = this.getClassGen().getJavaClass();
		JOptionPane.showMessageDialog(null, c1.getPackageName());
		StringTokenizer token = new StringTokenizer(c1.getPackageName(),".");
		do{
			baseDir = new File(baseDir,token.nextToken());
			if(!baseDir.exists()){
				System.out.println("creating dir: "+baseDir);
				baseDir.mkdir();
			}
		}while(token.hasMoreTokens());
		
		System.out.println("Dumping to file: "+baseDir);
		baseDir=new File(baseDir,c1.getFileName().replace(".java","")+".class");
		System.out.println("Dumping to file: "+baseDir);
		if(!baseDir.exists()){
			baseDir.createNewFile();
		}
		c1.dump(baseDir);
	}
	
	
	
	
	
	

}
